/*
  graphics.js
  Graphics object handles all rendering.
*/

(function ($) {
  'use strict';

  var p,
    m = $.mapster,
    u = m.utils,
    canvasMethods,
    vmlMethods;

  /**
   * Implemenation to add each area in an AreaData object to the canvas
   * @param {Graphics} graphics The target graphics object
   * @param {AreaData} areaData The AreaData object (a collection of area elements and metadata)
   * @param {object} options Rendering options to apply when rendering this group of areas
   */
  function addShapeGroupImpl(graphics, areaData, options) {
    var me = graphics,
      md = me.map_data,
      isMask = options.isMask;

    // first get area options. Then override fade for selecting, and finally merge in the
    // "select" effect options.

    $.each(areaData.areas(), function (_, e) {
      options.isMask = isMask || (e.nohref && md.options.noHrefIsMask);
      me.addShape(e, options);
    });

    // it's faster just to manipulate the passed options isMask property and restore it, than to
    // copy the object each time

    options.isMask = isMask;
  }

  /**
   * Convert a hex value to decimal
   * @param  {string} hex A hexadecimal toString
   * @return {int} Integer represenation of the hex string
   */

  function hex_to_decimal(hex) {
    return Math.max(0, Math.min(parseInt(hex, 16), 255));
  }
  function css3color(color, opacity) {
    return (
      'rgba(' +
      hex_to_decimal(color.substr(0, 2)) +
      ',' +
      hex_to_decimal(color.substr(2, 2)) +
      ',' +
      hex_to_decimal(color.substr(4, 2)) +
      ',' +
      opacity +
      ')'
    );
  }
  /**
   * An object associated with a particular map_data instance to manage renderin.
   * @param {MapData} map_data The MapData object bound to this instance
   */

  m.Graphics = function (map_data) {
    //$(window).unload($.mapster.unload);
    // create graphics functions for canvas and vml browsers. usage:
    // 1) init with map_data, 2) call begin with canvas to be used (these are separate b/c may not require canvas to be specified
    // 3) call add_shape_to for each shape or mask, 4) call render() to finish

    var me = this;
    me.active = false;
    me.canvas = null;
    me.width = 0;
    me.height = 0;
    me.shapes = [];
    me.masks = [];
    me.map_data = map_data;
  };

  p = m.Graphics.prototype = {
    constructor: m.Graphics,

    /**
     * Initiate a graphics request for a canvas
     * @param  {Element} canvas The canvas element that is the target of this operation
     * @param  {string} [elementName] The name to assign to the element (VML only)
     */

    begin: function (canvas, elementName) {
      var c = $(canvas);

      this.elementName = elementName;
      this.canvas = canvas;

      this.width = c.width();
      this.height = c.height();
      this.shapes = [];
      this.masks = [];
      this.active = true;
    },

    /**
     * Add an area to be rendered to this canvas.
     * @param {MapArea} mapArea The MapArea object to render
     * @param {object} options An object containing any rendering options that should override the
     *                         defaults for the area
     */

    addShape: function (mapArea, options) {
      var addto = options.isMask ? this.masks : this.shapes;
      addto.push({ mapArea: mapArea, options: options });
    },

    /**
     * Create a canvas that is sized and styled for the MapData object
     * @param  {MapData} mapData The MapData object that will receive this new canvas
     * @return {Element} A canvas element
     */

    createVisibleCanvas: function (mapData) {
      return $(this.createCanvasFor(mapData))
        .addClass('mapster_el')
        .css(m.canvas_style)[0];
    },

    /**
     * Add a group of shapes from an AreaData object to the canvas
     *
     * @param {AreaData} areaData An AreaData object (a set of area elements)
     * @param {string} mode     The rendering mode, "select" or "highlight". This determines the target
     *                          canvas and which default options to use.
     * @param {striong} options  Rendering options
     */

    addShapeGroup: function (areaData, mode, options) {
      // render includeKeys first - because they could be masks
      var me = this,
        list,
        name,
        canvas,
        map_data = this.map_data,
        opts = areaData.effectiveRenderOptions(mode);

      if (options) {
        $.extend(opts, options);
      }

      if (mode === 'select') {
        name = 'static_' + areaData.areaId.toString();
        canvas = map_data.base_canvas;
      } else {
        canvas = map_data.overlay_canvas;
      }

      me.begin(canvas, name);

      if (opts.includeKeys) {
        list = u.split(opts.includeKeys);
        $.each(list, function (_, e) {
          var areaData = map_data.getDataForKey(e.toString());
          addShapeGroupImpl(
            me,
            areaData,
            areaData.effectiveRenderOptions(mode)
          );
        });
      }

      addShapeGroupImpl(me, areaData, opts);
      me.render();
      if (opts.fade) {
        // fading requires special handling for IE. We must access the fill elements directly. The fader also has to deal with
        // the "opacity" attribute (not css)

        u.fader(
          m.hasCanvas()
            ? canvas
            : $(canvas).find('._fill').not('.mapster_mask'),
          0,
          m.hasCanvas() ? 1 : opts.fillOpacity,
          opts.fadeDuration
        );
      }
    }

    // These prototype methods are implementation dependent
  };

  function noop() {}

  // configure remaining prototype methods for ie or canvas-supporting browser

  canvasMethods = {
    renderShape: function (context, mapArea, offset) {
      var i,
        c = mapArea.coords(null, offset);

      switch (mapArea.shape) {
        case 'rect':
        case 'rectangle':
          context.rect(c[0], c[1], c[2] - c[0], c[3] - c[1]);
          break;
        case 'poly':
        case 'polygon':
          context.moveTo(c[0], c[1]);

          for (i = 2; i < mapArea.length; i += 2) {
            context.lineTo(c[i], c[i + 1]);
          }
          context.lineTo(c[0], c[1]);
          break;
        case 'circ':
        case 'circle':
          context.arc(c[0], c[1], c[2], 0, Math.PI * 2, false);
          break;
      }
    },
    addAltImage: function (context, image, mapArea, options) {
      context.beginPath();

      this.renderShape(context, mapArea);
      context.closePath();
      context.clip();

      context.globalAlpha = options.altImageOpacity || options.fillOpacity;

      context.drawImage(
        image,
        0,
        0,
        mapArea.owner.scaleInfo.width,
        mapArea.owner.scaleInfo.height
      );
    },
    render: function () {
      // firefox 6.0 context.save() seems to be broken. to work around,  we have to draw the contents on one temp canvas,
      // the mask on another, and merge everything. ugh. fixed in 1.2.2. unfortunately this is a lot more code for masks,
      // but no other way around it that i can see.

      var maskCanvas,
        maskContext,
        me = this,
        md = me.map_data,
        hasMasks = me.masks.length,
        shapeCanvas = me.createCanvasFor(md),
        shapeContext = shapeCanvas.getContext('2d'),
        context = me.canvas.getContext('2d');

      if (hasMasks) {
        maskCanvas = me.createCanvasFor(md);
        maskContext = maskCanvas.getContext('2d');
        maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);

        $.each(me.masks, function (_, e) {
          maskContext.save();
          maskContext.beginPath();
          me.renderShape(maskContext, e.mapArea);
          maskContext.closePath();
          maskContext.clip();
          maskContext.lineWidth = 0;
          maskContext.fillStyle = '#000';
          maskContext.fill();
          maskContext.restore();
        });
      }

      $.each(me.shapes, function (_, s) {
        shapeContext.save();
        if (s.options.fill) {
          if (s.options.altImageId) {
            me.addAltImage(
              shapeContext,
              md.images[s.options.altImageId],
              s.mapArea,
              s.options
            );
          } else {
            shapeContext.beginPath();
            me.renderShape(shapeContext, s.mapArea);
            shapeContext.closePath();
            //shapeContext.clip();
            shapeContext.fillStyle = css3color(
              s.options.fillColor,
              s.options.fillOpacity
            );
            shapeContext.fill();
          }
        }
        shapeContext.restore();
      });

      // render strokes at end since masks get stroked too

      $.each(me.shapes.concat(me.masks), function (_, s) {
        var offset = s.options.strokeWidth === 1 ? 0.5 : 0;
        // offset applies only when stroke width is 1 and stroke would render between pixels.

        if (s.options.stroke) {
          shapeContext.save();
          shapeContext.strokeStyle = css3color(
            s.options.strokeColor,
            s.options.strokeOpacity
          );
          shapeContext.lineWidth = s.options.strokeWidth;

          shapeContext.beginPath();

          me.renderShape(shapeContext, s.mapArea, offset);
          shapeContext.closePath();
          shapeContext.stroke();
          shapeContext.restore();
        }
      });

      if (hasMasks) {
        // render the new shapes against the mask

        maskContext.globalCompositeOperation = 'source-out';
        maskContext.drawImage(shapeCanvas, 0, 0);

        // flatten into the main canvas
        context.drawImage(maskCanvas, 0, 0);
      } else {
        context.drawImage(shapeCanvas, 0, 0);
      }

      me.active = false;
      return me.canvas;
    },

    // create a canvas mimicing dimensions of an existing element
    createCanvasFor: function (md) {
      return $(
        '<canvas width="' +
          md.scaleInfo.width +
          '" height="' +
          md.scaleInfo.height +
          '"></canvas>'
      )[0];
    },
    clearHighlight: function () {
      var c = this.map_data.overlay_canvas;
      c.getContext('2d').clearRect(0, 0, c.width, c.height);
    },
    // Draw all items from selected_list to a new canvas, then swap with the old one. This is used to delete items when using canvases.
    refreshSelections: function () {
      var canvas_temp,
        map_data = this.map_data;
      // draw new base canvas, then swap with the old one to avoid flickering
      canvas_temp = map_data.base_canvas;

      map_data.base_canvas = this.createVisibleCanvas(map_data);
      $(map_data.base_canvas).hide();
      $(canvas_temp).before(map_data.base_canvas);

      map_data.redrawSelections();

      $(map_data.base_canvas).show();
      $(canvas_temp).remove();
    }
  };

  vmlMethods = {
    renderShape: function (mapArea, options, cssclass) {
      var me = this,
        fill,
        stroke,
        e,
        t_fill,
        el_name,
        el_class,
        template,
        c = mapArea.coords();
      el_name = me.elementName ? 'name="' + me.elementName + '" ' : '';
      el_class = cssclass ? 'class="' + cssclass + '" ' : '';

      t_fill =
        '<v:fill color="#' +
        options.fillColor +
        '" class="_fill" opacity="' +
        (options.fill ? options.fillOpacity : 0) +
        '" /><v:stroke class="_fill" opacity="' +
        options.strokeOpacity +
        '"/>';

      stroke = options.stroke
        ? ' strokeweight=' +
          options.strokeWidth +
          ' stroked="t" strokecolor="#' +
          options.strokeColor +
          '"'
        : ' stroked="f"';

      fill = options.fill ? ' filled="t"' : ' filled="f"';

      switch (mapArea.shape) {
        case 'rect':
        case 'rectangle':
          template =
            '<v:rect ' +
            el_class +
            el_name +
            fill +
            stroke +
            ' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:' +
            c[0] +
            'px;top:' +
            c[1] +
            'px;width:' +
            (c[2] - c[0]) +
            'px;height:' +
            (c[3] - c[1]) +
            'px;">' +
            t_fill +
            '</v:rect>';
          break;
        case 'poly':
        case 'polygon':
          template =
            '<v:shape ' +
            el_class +
            el_name +
            fill +
            stroke +
            ' coordorigin="0,0" coordsize="' +
            me.width +
            ',' +
            me.height +
            '" path="m ' +
            c[0] +
            ',' +
            c[1] +
            ' l ' +
            c.slice(2).join(',') +
            ' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:' +
            me.width +
            'px;height:' +
            me.height +
            'px;">' +
            t_fill +
            '</v:shape>';
          break;
        case 'circ':
        case 'circle':
          template =
            '<v:oval ' +
            el_class +
            el_name +
            fill +
            stroke +
            ' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:' +
            (c[0] - c[2]) +
            'px;top:' +
            (c[1] - c[2]) +
            'px;width:' +
            c[2] * 2 +
            'px;height:' +
            c[2] * 2 +
            'px;">' +
            t_fill +
            '</v:oval>';
          break;
      }
      e = $(template);
      $(me.canvas).append(e);

      return e;
    },
    render: function () {
      var opts,
        me = this;

      $.each(this.shapes, function (_, e) {
        me.renderShape(e.mapArea, e.options);
      });

      if (this.masks.length) {
        $.each(this.masks, function (_, e) {
          opts = u.updateProps({}, e.options, {
            fillOpacity: 1,
            fillColor: e.options.fillColorMask
          });
          me.renderShape(e.mapArea, opts, 'mapster_mask');
        });
      }

      this.active = false;
      return this.canvas;
    },

    createCanvasFor: function (md) {
      var w = md.scaleInfo.width,
        h = md.scaleInfo.height;
      return $(
        '<var width="' +
          w +
          '" height="' +
          h +
          '" style="zoom:1;overflow:hidden;display:block;width:' +
          w +
          'px;height:' +
          h +
          'px;"></var>'
      )[0];
    },

    clearHighlight: function () {
      $(this.map_data.overlay_canvas).children().remove();
    },
    // remove single or all selections
    removeSelections: function (area_id) {
      if (area_id >= 0) {
        $(this.map_data.base_canvas)
          .find('[name="static_' + area_id.toString() + '"]')
          .remove();
      } else {
        $(this.map_data.base_canvas).children().remove();
      }
    }
  };

  // for all methods with two implemenatations, add a function that will automatically replace itself with the correct
  // method on first invocation

  $.each(
    [
      'renderShape',
      'addAltImage',
      'render',
      'createCanvasFor',
      'clearHighlight',
      'removeSelections',
      'refreshSelections'
    ],
    function (_, e) {
      p[e] = (function (method) {
        return function () {
          p[method] =
            (m.hasCanvas() ? canvasMethods[method] : vmlMethods[method]) ||
            noop;

          return p[method].apply(this, arguments);
        };
      })(e);
    }
  );
})(jQuery);
